Skip to content

S16-00 专题-JS-手写

[TOC]

手写-call/aplly/bind

  • function.call()(thisArg,arg1?,arg2?,...),用于显式调用一个函数,并动态指定函数执行时的 this 值及参数列表。
  • function.apply()(thisArg,args?),用于显式调用一个函数,并动态指定函数执行时的 this 值及参数列表。
  • function.bind()(thisArg,arg1?,arg2?,...),用于创建一个新的函数,该函数在调用时会以指定的 this 值和预先提供的参数作为默认参数。

函数对象原型关系

函数 foo 对象的隐式原型 === Function 的显式原型

js
// 函数foo对象的隐式原型 === Function的显式原型
console.log(foo.__proto__ === Function.prototype); // true

console.log(Function.prototype.apply); // f apply()
console.log(Function.prototype.call); // f call()
console.log(Function.prototype.bind); // f bind()

console.log(Function.prototype.apply === foo.apply); // true

结论

  1. foo 对象中的某些属性和方法是来自 Function.prototype 的。
  2. Function.prototype 中添加的属性和方法,可以被所有的函数获取。

image-20250729224050108

在 Function 的原型中添加方法 bar

image-20250729224208694

手写 apply

前置知识

  1. 给函数对象添加方法:可以通过在Function.prototype上添加方法实现。

    image-20250730155358016


apply/call使用示例

image-20250730155032244

基本实现

apply实现思路

  • 将函数设置为目标对象的属性:mrthis.fn = this
  • 执行该函数:mrthis.fn(...args)
  • 删除添加的属性:delete mrthis.fn
  • 返回函数执行结果
js
function foo() {
  console.log("foo", this);
}

Function.prototype.mrapply = function (mrthis) {
  // 相当于 mrthis.fn = this
  Object.defineProperty(mrthis, "fn", {
    configurable: true,
    value: this,
  });
  // 隐式调用fn,可以让fn函数的this指向 mrthis
  mrthis.fn(); // 相当于 this()
  // 删除多出来的临时函数fn
  delete mrthis.fn;
};

foo.mrapply({ name: "Tom" });

优化:this类型处理

this类型处理

  • 如果传入的this参数是一个 String 或者 Number 的类型,需要将其包裹成对象类型,才能在它上面添加属性

    image-20250730161529360

  • 如果传入的this参数是undefined或null,this指向window

    image-20250730161553413

js
function foo (age, height) {
  console.log('foo', this, age, height)
}

Function.prototype.mrapply = function(mrthis) {
  // 当this不是对象时,需要用Object包裹
  mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)

  // 相当于 mrthis.fn = this
  Object.defineProperty(mrthis, 'fn', {
      configurable: true,
      value: this
  })

  // 隐式调用fn,可以让fn函数的this指向 mrthis
  mrthis.fn()

  // 删除多出来的临时函数fn
  delete mrthis.fn
}

foo.mrapply({name: "Tom"})
foo.mrapply(null)
foo.mrapply(undefined)
foo.mrapply(true)
foo.mrapply(123)
foo.mrapply('aaaa')

优化:绑定参数

调用 mrapply 时,传递参数

image-20230224214829699

js
function foo (age, height) {
  console.log('foo', this, age, height)
}

Function.prototype.mrapply = function(mrthis, args) {
  // 当this不是对象时,需要用Object包裹
  mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)

  // 相当于 mrthis.fn = this
  Object.defineProperty(mrthis, 'fn', {
      configurable: true,
      value: this
  })

  // 隐式调用fn,可以让fn函数的this指向 mrthis
  mrthis.fn(...args)

  // 删除多出来的临时函数fn
  delete mrthis.fn
}

foo.mrapply({name: "Tom"}, [18, 1.88])
foo.mrapply(null, [18, 1.88])
foo.mrapply(undefined, [18, 1.88])
foo.mrapply(true, [18, 1.88])
foo.mrapply(123, [18, 1.88])
foo.mrapply('aaaa', [18, 1.88])

手写 call

实现思路:与 call() 类似,但处理参数为数组形式。

js
    function foo(age, height) {
      console.log('foo', this, age, height)
    }

    Function.prototype.mrcall = function(mrthis, ...args) { // 区别:apply:(mrthis, args)
      mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)

      Object.defineProperty(mrthis, 'fn', {
        configurable: true,
        value: this
      })

      mrthis.fn(...args)

      delete mrthis.fn
    }

    foo.mrcall({ name: "张飞" }, 20, 1.77)

抽取封装公共函数

  1. 抽取封装的函数

    js
    /* 抽取封装的函数 */
    Function.prototype.mrexec = function(mrthis, args) {
      mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
        
      // mrthis.fn = this
      Object.defineProperty(mrthis, 'fn', {
        configurable: true,
        value: this
      })
        
      mrthis.fn(...args)
        
      delete mrthis.fn
    }
  2. 手写apply

    js
    /* 手写apply */
    Function.prototype.mrapply = function(mrthis, args) {
      this.mrexec(mrthis, args)
    }
  3. 手写call

    js
    /* 手写call */
    Function.prototype.mrcall = function(mrthis, ...args) {
      this.mrexec(mrthis, args)
    }
  4. 测试

    js
    // 测试
    function foo(age, height) {
      console.log('foo', this, age, height)
    }
    foo.mrapply({name: "Tom"}, [19, 1.66])
    foo.mrcall({name: "Jack"}, 22, 1.99)

手写 bind

bind使用示例:和apply/call不同,bind执行后是返回一个新的函数newFoo。

image-20230225114537180

基本实现

思路:想办法实现如下:

js
// 伪代码
{ name: "why" }.foo(name, age)
js
/* 手写bind */
Function.prototype.mrbind = function(mrthis, ...args) {
  return (...moreArgs) => {
    mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
      
    Object.defineProperty(mrthis, 'fn', {
      configurable: true,
      value: this
    })
      
    // 合并2个方法中传递的参数
    const allArgs = [...args, ...moreArgs]
    
    mrthis.fn(...allArgs) // 相当于 this()
      
    delete mrthis.fn // 可以删除fn,因为每次调用newFoo,都会重新生成一个mrthis.fn
  }
}

// 测试
function foo(name, age, height, address) {
  console.log('foo', this, name, age, height, address)
}
const newFoo = foo.mrbind({name: "Jerry"}, '张飞', 45)
console.log(newFoo)
newFoo(1.88, '成都')
newFoo(1.88, '成都')

手写-防抖/节流

概述

防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中

  • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。

  • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题

但是很多前端开发者面对这两个功能,有点摸不着头脑:

  • 某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);

  • 某些开发者可以区分,但是不知道如何应用;

  • 某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;

接下来我们会一起来学习防抖和节流函数:

  • 我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;

  • 并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;

防抖

防抖(Debounce):是一种前端性能优化技术,用于限制高频触发事件的回调函数执行次数。其核心思想是:事件触发后,等待一段时间再执行回调。若在等待期间事件再次触发,则重新计时,直到等待期结束后才真正执行一次回调。


类比理解(电梯场景)

想象你在电梯门口:

  • 有人进电梯(触发事件)→ 电梯门开始关闭(开始计时)。
  • 若在关门期间又有人进来(再次触发) → 电梯门重新打开(重置计时)。
  • 直到连续 N 秒无人进入(等待期结束) → 电梯门关闭并运行(执行一次回调)。

图示理解

我们用一副图来理解一下它的过程:

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;

  • 当事件密集触发时,函数的触发会被频繁的推迟

  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数

image-20230620152349381

应用场景:

防抖的应用场景很多:

  • 搜索联想oninput,输入框中频繁的输入内容,搜索或者提交信息;

  • 频繁点击按钮onclick,频繁的点击按钮,触发某个事件;

  • 浏览器滚动事件onscroll,监听浏览器滚动事件,完成某些特定操作;

  • 浏览器缩放事件onresize,用户缩放浏览器的resize事件;


示例:搜索联想

我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容

image-20230620152423458

比如想要搜索一个MacBook:

  • 当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;

  • 当继续输入ma时,再次发送网络请求;

  • 那么macbook一共需要发送7次网络请求;

  • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;

但是我们需要这么多次的网络请求吗?

  • 不需要,正确的做法应该是在合适的情况下再发送网络请求;

  • 比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;

  • 比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;

  • 也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;

这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数

我们通过一个搜索框来延迟防抖函数的实现过程:

  • 监听input的输入,通过打印模拟网络请求

测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作:

image-20230620152534787

image-20230620152544034


生活中防抖的例子:

比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。

如果在五分钟的时间内,没有同学问我问题,那么我就下课了;

  • 在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;

  • 如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);

节流

节流(throttle)

我们用一副图来理解一下节流的过程

  • 当事件触发时,会执行这个事件的响应函数;

  • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数

  • 不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;

image-20230620152442909

应用场景:

  • 页面滚动事件:监听页面的滚动事件;

  • 鼠标移动事件

  • 频繁点击事件:用户频繁点击按钮操作;

  • 游戏某些设计:游戏中的一些设计,如发射子弹;

很多人都玩过类似于飞机大战的游戏

在飞机大战的游戏中,我们按下空格会发射一个子弹:

  • 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;

  • 比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;

  • 但是事件是触发了10次的,响应的函数只触发了一次;

image-20230620152511039


生活中节流的例子:

比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;

如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;

手写防抖

我们按照如下思路来实现:

  • 防抖基本功能实现:可以实现防抖效果
  • 优化一:优化参数和this指向
  • 优化二:优化取消操作(增加取消功能)
  • 优化三:优化立即执行效果(第一次立即执行)
  • 优化四:优化返回值

基本实现

image-20230913170819191

优化:参数和this绑定

this指向

image-20230913171801374

参数

image-20230913172010545

image-20230913172141908

优化:取消功能

image-20230913173224970

image-20230913173251166

优化:第一次立即执行

  • immediate:控制否时启用立即执行功能
  • isInvoke:控制函数是否已经立即执行一次了

image-20230913175323462

image-20230913175525645

优化:返回值

image-20230914120206319

image-20230914120006631

image-20230914120016985

手写节流

我们按照如下思路来实现:

  • 节流函数的基本实现:可以实现节流效果
  • 优化一:绑定this和参数
  • 优化二:控制立即执行,节流最后一次也可以执行
  • 优化三:优化添加取消功能
  • 优化四:优化返回值问题

基本实现

image-20230914145011970

image-20230914144933945

优化:绑定this和参数

image-20230914145650570

image-20230914145254694

优化:控制立即执行

image-20230914150800082

image-20230914145957177

优化:控制执行最后一次

思路一: 给每次点击时添加一个定时器,延迟时间设为waitTime,当再次点击时取消上次的定时器,重新添加一个。

思路二: 在每个执行fn函数的节点,添加一个延迟时间为waitTime的定时器,当用户在fn函数执行节点的时间上也点击了一次就取消该定时器(使用中

image-20230914161141810

优化:取消功能

image-20230914173427834

image-20230914173431437

优化:返回值

image-20230914174045131

image-20230914173857111

手写-深拷贝函数

浅拷贝/深拷贝

对象相互赋值

前面我们已经学习了对象相互赋值的一些关系,分别包括:

js
const info = {
  name: 'tom',
  age: 18,
  friend: {
      name: 'jack'
  }
}
  • 引用赋值:指向同一个对象,相互之间会影响;

    js
    const obj1 = info
    
    // 特性:obj1 和 info 指向同一个对象
  • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;

    js
    const obj2 = {...info} // 方式一:
    const obj3 = Object.assign({}, info) // 方式二:
    
    // 特性1:修改 obj2.name,原对象 info.name 的值依然是 'tom'
    obj2.name = '张飞'
    console.log(info.name) // 'tom'
    
    // 特性2:修改 obj2.friend.name,原对象 info.friend.name 的值会随之改变成 '曹操'
    obj2.friend.name = '曹操'
    console.log(info.friend.name) // '曹操'
  • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;


深拷贝实现方式

  • JSON.parse

    js
    const obj = JSON.parse(JSON.stringify(info))
  • 第三方库:underscore、lodash

  • 自己实现


JSON.parse的缺点

前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse

  • 这种深拷贝的方式其实对于函数Symbol等是无法处理的;

    image-20250724174537845

  • 并且如果存在对象的循环引用,也会报错的;

    image-20250724175206921

手写深拷贝

  • 1.自定义深拷贝的基本功能;

  • 2.对Symbol的key进行处理;

  • 3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;

  • 4.对循环引用的处理;

工具函数:判断对象

实现思路:通过 typeof 判断,并对 typeof 返回的结果进行分析,找出是对象的类型返回。

image-20250724180744248

基本实现

image-20250724181208147

image-20230915091526589

优化:区分数组和对象

实现思路:通过 Array.isArray() 区分是数组还是对象。

image-20250724210654225

image-20250724213755166

优化:其他类型-Set

问题:当对象中有Set类型时,无法拷贝。

分析:这是因为 deepCopy() 内部是通过对目标对象执行 for...in 操作进行遍历的,而Set通过for...in操作返回的是空值。

image-20230915095300660

image-20250724214745653

优化:其他类型-Map

思路和Set一致

优化:其他类型-Function

function: 不需要深拷贝

image-20250724220957111

image-20250724220911458

优化:其他类型-Symbol为值

问题:当对象中有Symbol类型的值时,会直接返回原始的Symbol值,此处的新值和旧值指向同一个Symbol。

image-20230915102436519

image-20230915102117655

优化:其他类型-Symbol为key

问题:Symbol作为key时,无法通过 for...in 遍历出来。

解决:必须对Symbol类型的key单独使用 Object.getOwnPropertySymbols(),再通过 for...of 遍历。

image-20250724222319587

image-20230915102523200

优化:处理循环引用

循环引用问题:当原始对象出现循环引用时,调用 deepCopy() 会出现无限递归,最终报错:

image-20250724222814561

image-20250724222831326

方案一:将每次新创建的对象保存到Map中,每次遍历前判断之前是否已经保存过了该对象。

问题:需要在deeCopy外部定义一个map,并且每次拷贝完成后map依然会形成对对象的强引用,没有销毁。

image-20250724224145263

方案二(推荐):使用WeakMap替代Map;将map放入参数中并设置一个默认值new WeakMap()

image-20230915105909339

手写-事件总线

事件总线

  • mitt()(all?),用于创建一个事件总线实例,支持事件的监听、触发和移除。
  • emitter.on()(type,handler),用于 监听指定类型的事件,当该事件被触发时,执行绑定的处理函数。支持监听具体事件类型或使用通配符 * 监听所有事件。
  • emitter.off()(type,handler?),用于 移除指定事件类型的监听器,支持移除单个处理函数或清空某事件类型的所有监听器,避免内存泄。
  • emitter.emit()(type,data?),用于 触发指定类型的事件,并传递相关数据给所有监听该事件的处理器。
  • emitter.all{eventName: Handler[]},用于存储所有已注册的事件类型及其对应的事件处理器列表,用于调试或高级操作。

自定义事件总线属于一种观察者模式,其中包括三个角色:

  • 发布者(Publisher):发出事件(Event);

  • 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);

  • 事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;

当然我们可以选择一些第三方库

  • Vue2默认是带有事件总线的功能;

  • Vue3中推荐一些第三方库,比如mitt

当然我们也可以实现自己的事件总线:

  • 事件的监听方法on;

  • 事件的发射方法emit;

  • 事件的取消监听off;

手写事件总线

基本实现

image-20250726083708584

image-20250726083822511

优化:绑定参数

image-20230915114132235

image-20250726083944393

优化:移除监听

实现思路

  • 遍历 eventMap中事件名对应的事件函数数组,找到需要删除的事件函数,将其删除。
  • 如果事件名对应的事件函数数组已经为空,则删除该事件名属性。

image-20250726084606706

image-20250726084743977

完整代码

image-20230915114603806

image-20230915114736411

手写-Promise

Promise结构设计

Promses/A+ 规范: https://promisesaplus.com/

Promise三种状态

image-20230804155236017

调用回调函数时传递参数

image-20230804155855738

实例方法

then

then基本实现

思路:

  • 1 then接收onFulfilledonRejected参数,并将其保存到this上

  • 2.1 在resolve()回调方法的queueMicrotask()回调函数参数中调用onFulfilled方法

  • 2.2 在reject()回调方法的queueMicrotask()回调函数参数中调用onRejected方法

注意: queueMicrotask(cb)方法的作用是将cb回调方法加入到微任务队列中。

image-20230804162214217

同一个promise多次调用then

image-20230804162749533

思路: 将需要多次调用的成功回调和失败回调分别放入一个数组中,调用时再遍历该数组,分别调用数组中的回调方法

1、定义2个数组,将then中的成功、失败回调分别push到这2个数组中

image-20230804164309826

image-20230804164327089

2、遍历这2个数组,再分别调用数组中的回调方法

image-20230804164704305

image-20230804164732409

异步延时调用then

问题: then方法如果在延迟1秒后调用,当promise的resolve()执行时,该then方法的回调函数不会被执行。

image-20230804165009050

分析: 这是因为当promise内部的resolve()执行时,then方法由于延迟原因还没有加入到数组onFulfilledFns,也就不会被执行。

解决: 可以在then方法中,事先判断promise的状态

  • 如果已经是fulfilledrejected,表示已经执行了resolve()rejecct()方法,此时可以直接调用延迟调用的then方法的回调函数。
  • 只有在pending状态才将then方法的回调函数push到数组中保存。

image-20230804165532377


问题: 此时res1、res2无法接收到resolve()执行后的参数

image-20241104154418255

分析: 这是因为执行了resolve()方法后,status立马变成fulfilled,再执行then()方法时status已经处于fulfilled状态,then中的回调会被直接调用,此时queueMicrotask()方法还没有执行,this.value还没有赋值。

解决: 将状态status放入微队列queueMicrotask中

image-20230804165935831

image-20230804170001857


问题: 将状态status放入微队列queueMicrotask中后,resolve和reject都会执行,加入微任务队列

image-20230804171059521

分析: 这是由于resolve()和reject()在加入微任务时,status的状态都为pending。因此都会被加入微任务队列。

解决: 在加入微任务前判断当前状态是否为pending,如果不是pending则表示已经执行了某个回调,就不能加入微任务

image-20230804171504378

image-20230804171611533

then方法的链式调用

image-20230804173822349

思路:

  • 当前then方法没有返回值,所以默认会返回undefined,不能通过undefined.then()链式调用方法。
  • 通过then方法中返回一个新的Promise,可以实现链式调用then方法
  • 新Promise中resolve(res)或reject(err)的参数res或err必须是上一次then中回调返回的结果

image-20241104165759021


问题: 在new Promise中抛出异常的情况

image-20230804174130505

解决: 在constructor中捕获执行executor()的异常。

image-20230804174247394

封装try catch中相似的代码

image-20230804174556340

image-20230804175948146

then回调函数参数可选【
then执行结果值类型【

判断下面result的类型:普通值、promise、thenable

补充:

  • 可以通过result instanceof Promise判断否是一个Promise
  • 可以通过typeof result.then === 'function'判断是否是一个thenable对象

image-20230804175249845

catch

调用catch方法

image-20241104171857387


▸ 基本实现

思路:通过调用then方法时只传递reject回调实现catch

image-20241104171817256


问题: 在回调函数有值(存在)的情况下,才去执行函数或添加到数组中

image-20230804180124250


问题: 调用catch的是返回的新promise,不是和then同一个promise

image-20241104172951614

解决: 当promise1中的reject为undefined时,在then方法执行reject回调处抛出一个异常。这样就会被第二个promise接收到了

image-20230804181618191

finally

调用finally方法

image-20230804203232552


▸ 基本实现

思路: 可以借用then()方法,在then方法的resolve和reject回调中都调用onfinally()实现

image-20241104175809777


问题: 添加catch后,执行resolve时,finally被阻止了,不再执行finally中的回调。只有执行reject时才会执行finally

image-20230804203602679

image-20230804203353587

原因: 这是由于catch方法中是这样调用then的:this.then(undefined, onRejected),其中成功回调是undefined,所以就不会处理上次then返回的值

image-20241104180538179

解决: 当onFulfilled为undefined时,给它一个默认的回调函数:value => { return value }

image-20230804204302336

类方法

resolve

思路: 直接在 new Promise()中调用 resolve() 方法

image-20241105220625982

使用resolve

image-20230804204953824

reject

思路: 直接在 new Promise()中调用 reject() 方法

image-20241105220720564

使用reject

image-20230804205018805

all

特点:

  • all中所有的promise都有结果后才会执行then或catch方法
  • 所有的promise之间执行与运算:都为resolve进入then方法;有一个reject进入catch方法

关键: 什么时候要执行resolve、什么时候要执行reject

思路: 遍历all的promises参数

  • 当有一个promise结果为reject时直接执行reject(),
  • 否则进入then,并保存结果到values中,当所有promise都有结果并且结果为resolve时,执行resolve()

image-20241105222517652

使用all

image-20241105221817643

allSettled

特性: allSettled会等所有promise都有结果(不区分resolve和reject),进入then方法,不会进入catch方法

image-20241105223847145

使用allSettled

image-20241105223724491

image-20230804211020865

race

特性: 只要有一个promise有结果,race立马有结果,无论resolve还是reject

image-20241105224634663

等价于下面的写法:

image-20230804211736445

使用race

image-20230804211613090

image-20230804211633864

any

特性:

  • 必须等到promise有一个resolve的结果,any才会有一个resolve的结果
  • 否则必须等到所有的promise都为reject结果,any才会有一个reject的结果

image-20241105230012823

使用any

image-20241105230148786

image-20241105230400826

image-20230804212626577

image-20230804212634453

最终代码@

js
  /* 工具函数-封装try...catch函数 */
  function runFunctionWithCatchError(fn, value, resolve, reject) {
    try {
      resolve(fn(value))
    } catch (err) {
      reject(err)
    }
  }

  // Promise状态
  const PROMISE_STATUS_PENDING = 'pending'
  const PROMISE_STATUS_FULFILLED = 'fulfilled'
  const PROMISE_STATUS_REJECTED = 'rejected'

  class MrPromise {
    constructor(executor) {
      this.status = PROMISE_STATUS_PENDING
      this.value = undefined
      this.reason = undefined
      this.onFulfilledFns = []
      this.onRejectedFns = []

      const resolve = (value) => {
        if (this.status === PROMISE_STATUS_PENDING) {
          queueMicrotask(() => {
            if (this.status !== PROMISE_STATUS_PENDING) return
            this.status = PROMISE_STATUS_FULFILLED
            this.value = value
            for (const fn of this.onFulfilledFns) {
              fn(this.value)
            }
          })
        }
      }

      const reject = (reason) => {
        if (this.status === PROMISE_STATUS_PENDING) {
          queueMicrotask(() => {
            if (this.status !== PROMISE_STATUS_PENDING) return
            this.status = PROMISE_STATUS_REJECTED
            this.reason = reason
            for (const fn of this.onRejectedFns) {
              fn(this.reason)
            }
          })
        }
      }

      try {
        executor(resolve, reject)
      } catch (err) {
        reject(err)
      }
    }

    then(onFulfilled, onRejected) {
      // 判断onFulfilled、onRejected回调函数是否存在
      onRejected = onRejected || ((err) => { throw err })
      onFulfilled = onFulfilled || ((res) => res)
      
      return new MrPromise((resolve, reject) => {
        // console.log('then status: ', this.status)
        if (this.status === PROMISE_STATUS_FULFILLED) {
          runFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
        }
        if (this.status === PROMISE_STATUS_REJECTED) {
          runFunctionWithCatchError(onRejected, this.reason, resolve, reject)
        }

        if (this.status === PROMISE_STATUS_PENDING) {
          this.onFulfilledFns.push(() => {
            runFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
          })

          this.onRejectedFns.push(() => {
            runFunctionWithCatchError(onRejected, this.reason, resolve, reject)
          })
        }
      })
    }

    catch(onRejected) {
      return this.then(undefined, onRejected)
    }

    finally(onFinally) {
      this.then(
        () => {
          onFinally()
        },
        () => {
          onFinally()
        }
      )
    }

    static resolve(value) {
      return new Promise((resolve) => resolve(value))
    }

    static reject(reason) {
      return new Promise((resolve, reject) => reject(reason))
    }

    static all(promises) {
      return new Promise((resolve, reject) => {
        const values = []
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              values.push(res)
              if (values.length === promises.length) {
                resolve(values)
              }
            },
            (err) => {
              reject(err)
            }
          )
        })
      })
    }

    static allSettled(promises) {
      return new Promise((resolve, reject) => {
        const results = []
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              results.push({ status: 'fulfilled', value: res })
              if (results.length === promises.length) {
                resolve(results)
              }
            },
            (err) => {
              results.push({ status: 'rejected', reason: err })
              if (results.length === promises.length) {
                resolve(results)
              }
            }
          )
        })
      })
    }

    static race(promises) {
      return new Promise((resolve, reject) => {
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              resolve(res)
            },
            (err) => {
              reject(err)
            }
          )
        })
      })
    }

    static any(promises) {
      return new Promise((resolve, reject) => {
        const reasons = []
        promises.forEach((promise) => {
          promise.then(
            (res) => {
              resolve(res)
            },
            (err) => {
              reasons.push(err)
              if (reasons.length === promises.length) {
                reject(new AggregateError(err))
              }
            }
          )
        })
      })
    }
  }

测试

js
  // 测试
  const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('p1~')
    }, 3000)
  })
  const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('p2~')
    }, 5000)
  })
  const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('p3~')
    }, 3000)
  })

  const p = new MrPromise((resolve, reject) => {
    // throw new Error('抛出异常')

    resolve('aaa')
    // reject('111')

    // setTimeout(() => {
    //   // resolve('aaa')
    //   reject('111')
    // }, 1000)
  })

  // - 类方法-any
  Promise.any([p1, p2, p3]).then(
    (res) => {
      console.log('any res: ', res)
    },
    (err) => {
      console.log('any err: ', err)
    }
  )

  // // - 类方法-race
  // Promise.race([p1, p2, p3]).then(
  //   (res) => {
  //     console.log('race res: ', res)
  //   },
  //   (err) => {
  //     console.log('race err: ', err)
  //   }
  // )

  // // - 类方法-allSettled
  // Promise.allSettled([p1, p2, p3]).then((res) => {
  //   console.log('allSettled: ', res)
  // })

  // // - 类方法-all
  // Promise.all([p1, p2, p3]).then(
  //   (res) => {
  //     console.log('all res: ', res)
  //   },
  //   (err) => {
  //     console.log('all err: ', err)
  //   }
  // )

  // // - 类方法-reject
  // Promise.reject('222').catch((err) => {
  //   console.log(err)
  // })

  // // - 类方法-resolve
  // Promise.resolve('1111').then((res) => {
  //   console.log(res)
  // })

  // p.then(
  //   (res) => {
  //     console.log('res: ', res)
  //   },
  //   (err) => {
  //     console.log('err: ', err)
  //   }
  // )

  // // - 异步延迟调用
  // setTimeout(() => {
  //   p.then(
  //     (res) => {
  //       console.log('异步延时调用 res: ', res)
  //     },
  //     (err) => {
  //       console.log('异步延时调用 err: ', err)
  //     }
  //   )
  // }, 2000)

  // // - 链式调用
  // p.then(
  //   (res) => {
  //     console.log('链式调用 res1: ', res)
  //     return 'bbb'
  //   },
  //   (err) => {
  //     console.log('链式调用 err1: ', err)
  //     return '222'
  //   }
  // ).then(
  //   (res) => {
  //     console.log('链式调用 res2: ', res)
  //     return 'ccc'
  //   },
  //   (err) => {
  //     console.log('链式调用 err2: ', err)
  //     return '333'
  //   }
  // )

  // // - catch
  // p.then((res) => {
  //   console.log('then res: ', res)
  // }).catch((err) => {
  //   console.log('catch err: ', err)
  // })

  // // - finally
  // p.then((res) => {
  //   console.log('then res: ', res)
  // })
  //   .catch((err) => {
  //     console.log('catch err: ', err)
  //   })
  //   .finally(() => {
  //     console.log('finally~')
  //   })

  // p.then(
  //   (res) => {
  //     console.log('res: ', res)
  //   },
  //   (err) => {
  //     console.log('err: ', err)
  //   }
  // )
  // p.then(
  //   (res) => {
  //     console.log('res2: ', res)
  //   },
  //   (err) => {
  //     console.log('err2: ', err)
  //   }
  // )